Skip to content

feat: add Docker support with ghcr.io publish workflow#66

Open
Eli-Golin wants to merge 3 commits intococoindex-io:mainfrom
Eli-Golin:feat/docker-support
Open

feat: add Docker support with ghcr.io publish workflow#66
Eli-Golin wants to merge 3 commits intococoindex-io:mainfrom
Eli-Golin:feat/docker-support

Conversation

@Eli-Golin
Copy link

Add Docker support

What this PR adds

  • docker/Dockerfile — multi-stage build that produces a self-contained image
  • .github/workflows/docker-publish.yml — publishes to ghcr.io/cocoindex-io/cocoindex-code on every release tag
  • README section — Docker usage alongside the existing uvx / claude mcp add instructions

Motivation

Some teams can't or don't want to install Python, uv, or manage system
dependencies on developer machines. Docker gives them:

  • Reproducibility — identical environment across macOS, Linux, Windows (WSL2)
  • Isolation — no Python version conflicts with other tools
  • Zero host deps — only Docker required

Design decisions

Multi-stage build

Three stages keep the final image lean and cache-friendly:

  1. builder — installs cocoindex-code and sentence-transformers via uv
  2. model_cache — pre-bakes all-MiniLM-L6-v2 into the image so cold
    starts don't trigger a ~90 MB download
  3. runtime — copies packages + model from previous stages into a fresh
    python:3.12-slim base

python:3.12-slim, not Alpine

cocoindex ships pre-built Rust wheels linked against glibc. Alpine uses musl-libc
and would require building from source.

ENTRYPOINT ["cocoindex-code", "serve"]

serve is the MCP stdio subcommand. This keeps the container invocation clean
(docker run --rm -i ... image) with no extra arguments needed. Users who want
the one-shot indexer can override with --entrypoint cocoindex-code ... index.

All config via environment variables

No project-specific defaults are baked into the image. COCOINDEX_CODE_ROOT_PATH
defaults to /workspace (the conventional mount point), everything else is
left to the user's docker run -e flags or .mcp.json config.

Named volume for model cache

The default model is baked in, but users who override COCOINDEX_CODE_EMBEDDING_MODEL
with an external provider benefit from a named volume so the model is only
downloaded once across rebuilds:

docker volume create cocoindex-model-cache

First-time setup note

After the workflow runs for the first time, the ghcr.io/cocoindex-io/cocoindex-code
package will be created automatically but will be private by default.
A maintainer needs to set it to public once:

GitHub → org page → Packages → cocoindex-code → Package settings → Change visibility → Public

After that, docker pull ghcr.io/cocoindex-io/cocoindex-code:latest works for
everyone without authentication.

Testing

Tested locally against a Scala/SBT codebase:

  1. docker build -t cocoindex-code:local -f docker/Dockerfile . — builds cleanly
  2. MCP stdio handshake (initialize → valid JSON-RPC result) ✅
  3. tools/list returns search tool ✅
  4. .cocoindex_code/ index directory created in mounted workspace ✅
  5. Wired into Claude Code via .mcp.json — semantic search returns results ✅

@georgeh0
Copy link
Member

Hi @Eli-Golin

Thanks a lot for creating this PR! Really appreciate.

Recently we just finished a major revision and under the new version we're having a CLI + a daemon (shared by all projects / all agent sessions for the same users).

I'm not an expert of docker. Wondering is it possible to update the Dockerfile to fit into the new architecture?

Thanks a lot!

@Eli-Golin
Copy link
Author

Hi @Eli-Golin

Thanks a lot for creating this PR! Really appreciate.

Recently we just finished a major revision and under the new version we're having a CLI + a daemon (shared by all projects / all agent sessions for the same users).

I'm not an expert of docker. Wondering is it possible to update the Dockerfile to fit into the new architecture?

Thanks a lot!

Should be one line fix I believe. Will handle it asap.

  - Remove `serve` subcommand from ENTRYPOINT — `cocoindex-code` with no
    args is the correct invocation under the new architecture; it auto-reads
    env vars, creates settings, and delegates to the internal daemon
  - Copy `ccc` binary into the runtime stage alongside `cocoindex-code`
  - Bump cocoindex floor pin from >=1.0.0a16 to >=1.0.0a33
@Eli-Golin Eli-Golin force-pushed the feat/docker-support branch from 470b0f3 to 7c35b69 Compare March 17, 2026 09:00
@Eli-Golin
Copy link
Author

Done.

@georgeh0
Copy link
Member

Hi @Eli-Golin,

Thanks for the update!

Note that in our current "CLI + background daemon" architecture, the daemon usually only starts once (automatically bring up by the CLI on demand) and standby. The CLI is very light, since it doesn't load any embedding models, open any databases, etc., so it can be invoked frequently across different agent sessions / projects.

So for the docker container, in my mind the ideal approach might be something like: users bring up the docker instance and let it standby, and use docker exec to invoke the CLI in the container.

e.g. as a CLI

docker exec -it <container_name_or_id> ccc index
docker exec -it <container_name_or_id> ccc search "authentication logic"

can also run in a MCP in stdio mode:

docker exec -it <container_name_or_id> ccc mcp

Or we can recommend users set such an alias:

alias ccc=docker exec -it <container_name_or_id> ccc

Then it can be directly used in the same way as a CLI running on the host machine, such as ccc index, ccc search ..., ccc mcp


There's one more complication by the database. Currently we use local files under .cocoindex_code/ folder for the index db, but it'll have trouble when accessing through the docker Linux (especially when host is macOS or Windows). A reliable approach is to put these databases within the docker container's native filesystem.

I'm considering introducing a environment variable to allow putting databases on a directory separate from the codebase directory. e.g.

COCOINDEX_CODE_DB_DIRECTORY=/workspace:/db-files

This will use /db-files as the prefix to replace /workspace to decide database location (e.g. database file for project at /workspace/cocoindex will be at /db-files/cocoindex).

I think this will enable we put the database files within the container?

Want to hear your opinion on these. I can make the db path mapping change.

Thanks!

@Eli-Golin
Copy link
Author

Eli-Golin commented Mar 18, 2026

Hi @georgeh0 , thanks for the detailed feedback!

You raise a good point about the daemon efficiency. However, there's a fundamental tension with the docker exec approach and how MCP stdio works:

The standard MCP stdio pattern is that the MCP client (Claude Code, Codex, etc.) spawns the server as a subprocess and communicates over stdin/stdout. docker run --rm -i fits this naturally — the client spawns the container, talks to it, and it dies cleanly when the session ends.

docker exec breaks this because it requires the container to already be running before the client connects. That means the user would need to manually docker run -d the container upfront, keep it running across reboots, and so on — which defeats the main appeal of the Docker approach (zero-setup, just configure .mcp.json and go).

So I think the docker run --rm -i model is the right fit for MCP stdio, and the model reload cost per session is an accepted trade-off of containerising an MCP server.

That said, the DB path issue you raised is a real and separate concern — slow SQLite access through volume mounts on macOS/Windows is a genuine problem. I'd love it if you made the COCOINDEX_CODE_DB_DIRECTORY change on your end. Once that's in, I can update the Dockerfile and README to use a named volume for the DB, which would give users the best of both worlds: mounted codebase + fast native DB storage.

Thanks!

@Eli-Golin
Copy link
Author

Actually, thinking about this more — there might be a way to offer both approaches and let users pick based on their needs.

Option 1 — stdio (current, zero setup)
docker run --rm -i ... in .mcp.json. Simple, fits the standard MCP pattern. Trade-off: daemon and model reload every session.

Option 2 — HTTP (persistent container)
User runs the container once in the background with a port exposed, and configures the MCP client to connect over HTTP/SSE instead of stdio. The daemon stays warm, model loaded once, shared across all sessions and projects. Trade-off: requires the user to manage a persistent container.

Since FastMCP (which this project already uses) supports both stdio and HTTP/SSE transports natively, adding this shouldn't require much work — just an env var to switch modes (e.g. COCOINDEX_CODE_TRANSPORT=http) and exposing a port in the Dockerfile.

This way users who just want zero-setup use Option 1, and teams who want the efficiency of a warm daemon use Option 2. We can document the trade-offs clearly and let them choose.

What do you think? Happy to implement this if you're on board.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants